package com.idi.maven.plugins;

import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.regex.Pattern;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Compares two zips based on exclusions/inclusion
 *
 * @author Yossi Shaul
 */
public class ZipFilesComparator {
    private static final Logger log = Logger.getLogger(ZipFilesComparator.class.getName());

    private List<Pattern> excludePatterns = Lists.newArrayList();

    public ZipFilesComparator addExclusionPatterns(Pattern... patterns) {
        excludePatterns.addAll(Arrays.asList(patterns));
        return this;
    }

    public boolean compare(File zip1, File zip2) {
        if (zip1 == null || zip2 == null) {throw new IllegalArgumentException("Input files cannot be null");}
        if (!zip1.exists()) {throw new IllegalArgumentException("File not found: " + zip1.getAbsolutePath());}
        if (!zip2.exists()) {throw new IllegalArgumentException("File not found: " + zip2.getAbsolutePath());}

        return getMD5Hash(zip1).equals(getMD5Hash(zip2)) || compareContent(zip1, zip2);
    }

    protected String getMD5Hash(File file) {
        InputStream fis = null;
        try {
            fis = new FileInputStream(file);
            long bufferSize = Math.min(65536,file.length());
            String md5 = getMD5Hash(fis, bufferSize);
            log.finer("MD5 for " + file.getName() + ": " + md5);
            return md5;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (fis != null) {fis.close();}
            }
            catch (IOException e) {
                log.warning("Failed to close zip file: " + e.getMessage());
            }
        }
    }

    private String getMD5Hash(InputStream fis, long bufferSize) {
        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[(int) bufferSize];
            int length;
            while ((length = fis.read(buffer)) > 0) {
                m.update(buffer, 0, length);
            }
            return new BigInteger(1, m.digest()).toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 algorithm not found", e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean compareContent(File file1, File file2) {
        ZipFile zip1 = null;
        ZipFile zip2 = null;
        try {

            zip1 = new ZipFile(file1);
            zip2 = new ZipFile(file2);

            List<String> matchingEntries1 = getMatchingEntriesNames(Lists.newArrayList(Iterators.forEnumeration(zip1.entries())));
            List<String> matchingEntries2 = getMatchingEntriesNames(Lists.newArrayList(Iterators.forEnumeration(zip2.entries())));

            if (matchingEntries1.size() == matchingEntries2.size() &&
                    (matchingEntries1.containsAll(matchingEntries2))) {
//                            && (matchingEntries2.containsAll(matchingEntries1)) - obvious, as sizes are same
                for (final String entryName : matchingEntries1) {
                    if (!compareZipEntries(zip1, zip2, entryName)) {
                        return false;
                    }
                }
                return true;
            } else {
                log.finer("Zips entries count is different");
                return false;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // close the zip files
            try {
                if (zip1 != null) {
                    zip1.close();
                }
                if (zip2 != null) {
                    zip2.close();
                }
            } catch (IOException e) {
                log.warning("Failed to close zip file: " + e.getMessage());
            }
        }
    }

    private List<String> getMatchingEntriesNames(List<ZipEntry> zipEntries) {
        List<String> entriesNames = Lists.newArrayList();
        for (final ZipEntry zipEntry : zipEntries) {
            String name = zipEntry.getName();
            if (!shouldExcludeEntry(name)) {
                entriesNames.add(name);
            }
        }
        return entriesNames;
    }

    private boolean shouldExcludeEntry(String name) {
        for (final Pattern excludePattern : excludePatterns) {
            if (excludePattern.matcher(name).matches()) {
                return true;
            }
        }
        return false;
    }

    private boolean compareZipEntries(ZipFile zip1, ZipFile zip2, String entryName) throws IOException {
        ZipEntry zip1Entry = zip1.getEntry(entryName);
        ZipEntry zip2Entry = zip2.getEntry(entryName);

        if (zip1Entry == null || zip2Entry == null) {
            log.finer("Entry " + entryName + " exists only in one of the files");
            return false;
        }

        InputStream entry1Stream = zip1.getInputStream(zip1Entry);
        InputStream entry2Stream = zip2.getInputStream(zip2Entry);
        long bufferSize = Math.min(65536, Math.min(zip1Entry.getSize(), zip2Entry.getSize()));

        boolean isMatching = getMD5Hash(entry1Stream, bufferSize).equals(getMD5Hash(entry2Stream, bufferSize));
        if (!isMatching) {
            log.finer(String.format("different md5 for entry name %s between [%s <--> %s]",entryName,zip1.getName(),zip2.getName()));
        }
        return isMatching;
    }
}
